import * as FormatUtils from '../utils/FormatUtils'
import * as RandomUtils from '../utils/RandomUtils'
import { log, PathUtils } from 'cetools'
import { writeTextFile } from '../utils/IPC'
import { formatFileName, upperFirst } from '../utils/FormatUtils'

export type Field = {
    fieldName: string
    fieldType: string
    description?: string
    valueExample?: string
    require?: boolean
    genericClassChain: Array<string>
}

export type ApiMethodParameter = {
    description: string
    type: string
    require: boolean
    isPrimitive: boolean
}

export type ApiMethod = {
    methodName: string
    description?: string
    path: string
    requestMethod: string
    postDataType: string
    parameters: ApiMethodParameter | Array<ApiMethodParameter>
    resultType: string
    supportsVersion: string
    resultGenerics: Array<string> | null
}

interface ApiDocument {
    description?: string
    path?: string
    methodList: Array<ApiMethod>
}

export type Entity = {
    className: string
    description?: string
    fieldList: Array<Field>
    generic: Array<string>
}

interface ApiDocuments {
    path: string
    documents: Array<ApiDocument>
    entities?: Map<string, Entity>
}

export const genericClassChainToStr = (baseType: string, chain: Array<string>): string => {
    let result = ''
    for (let i = chain.length - 1; i >= 0; i--) {
        result = '<' + javaTypeToTsType(FormatUtils.longTypeNameToShort(chain[i])) + result + '>'
    }
    result = javaTypeToTsType(FormatUtils.longTypeNameToShort(baseType)) + result
    return result
}

export const getFieldFullType = (field: Field): string => {
    if (field.genericClassChain.length > 0) {
        return genericClassChainToStr(field.fieldType, field.genericClassChain)
    }
    return field.fieldType
}

const entityFromType = (type: string, dict?: Map<string, Entity>): never => {
    const typeNames = type.split('|')
    if (dict) {
        if (typeNames.length > 1) {
            return dict[typeNames[1]]
        } else {
            return dict[typeNames[0]]
        }
    }
    return null
}

const entityTypeIsArray = (type: string): boolean => {
    const typeNames = type.split('|')
    return typeNames.length > 1 && typeNames[0].toLowerCase() == 'list'
}

const buildValueFromType = (type: string, dict?: Map<string, Entity>): never => {
    const typeNames = type.split('|')
    const typeName = FormatUtils.longTypeNameToShort(typeNames[0]).toLowerCase()
    switch (typeName) {
        case 'string':
            return 'abc'
        case 'byte':
            return RandomUtils.randomInt(255, 0)
        case 'integer':
        case 'int':
            return RandomUtils.randomInt(9000, 0)
        case 'date':
            return RandomUtils.randomDate(new Date('1980-01-01'))
        case 'boolean':
            return RandomUtils.randomInt(0, 1) == 0
        case 'float':
        case 'double':
            return RandomUtils.randomRange(20000, 0)
        case 'list':
            if (dict) {
                return new Array<never>(buildObject(dict[typeNames[1]], dict))
            }
        // eslint-disable-next-line no-fallthrough
        default:
            {
                if (dict && dict[type]) {
                    return buildObject(dict[type], dict)
                }
            }
            console.warn('not find class:', type, dict)
            return null
    }
}

const javaToTsTypeMap = new Map<string, string>()

javaToTsTypeMap.set('string', 'string')
javaToTsTypeMap.set('integer', 'number')
javaToTsTypeMap.set('long', 'number')
javaToTsTypeMap.set('double', 'number')
javaToTsTypeMap.set('float', 'number')
javaToTsTypeMap.set('date', 'Date')
javaToTsTypeMap.set('int', 'number')
javaToTsTypeMap.set('byte', 'number')
javaToTsTypeMap.set('list', 'Array')
javaToTsTypeMap.set('boolean', 'boolean')

export const javaTypeToTsType = (javaType: string) => {
    if (!javaType) {
        throw new Error('parameter is null or undefined')
    }
    const typeName = FormatUtils.longTypeNameToShort(javaType)
    const typeNameLower = typeName.toLowerCase()
    if (javaToTsTypeMap.has(typeNameLower)) {
        return javaToTsTypeMap.get(typeNameLower)
    }
    return typeName
}

export const mergeEntity = (entities: Array<Entity>, handledEntities?: Array<Entity>): Entity => {
    const list = JSON.parse(JSON.stringify(entities)) as Array<Entity>
    for (let i = 0; i < list.length; i++) {
        for (let j = 0; j < list[i].fieldList.length; j++) {
            if (list[i].fieldList[j].fieldType == 'T') {
                if (list.length > i) {
                    if (list[i + 1] && list[i + 1].className) {
                        list[i].fieldList[j].fieldType = list[i + 1].className
                    } else {
                        log.info('not have class name', i, list, list[i + 1])
                    }
                }
            }

            if (
                FormatUtils.longTypeNameToShort(list[i].fieldList[j].fieldType).toLowerCase() ==
                'list'
            ) {
                if (list[i + 1]) {
                    list[i].fieldList[j].fieldType = 'list|' + list[i + 1].className
                }
            }
        }
    }

    if (handledEntities) {
        handledEntities.push(...list)
    }
    return list[0]
}

const buildObject = (entity: Entity, dict?: Map<string, Entity>) => {
    const obj = {}
    if (!entity) {
        return null
    }
    if (!entity.fieldList) {
        console.warn('not have fieldList', entity)
        return obj
    }
    for (const i in entity.fieldList) {
        const fieldInfo: Field = entity.fieldList[i]

        if (fieldInfo.valueExample) {
            obj[fieldInfo.fieldName] = fieldInfo.valueExample
        } else if (dict && dict![fieldInfo.fieldType]) {
            const cEntity = dict![fieldInfo.fieldType]
            const typeName = FormatUtils.longTypeNameToShort(cEntity.className)
            if (fieldInfo.fieldName === '[]') {
                return buildObject(cEntity, dict)
            }

            if (javaToTsTypeMap[typeName.toLowerCase()]) {
                obj[fieldInfo.fieldName] = buildValueFromType(fieldInfo.fieldType, dict)
            } else {
                obj[fieldInfo.fieldName] = buildObject(cEntity, dict)
            }
        } else {
            obj[fieldInfo.fieldName] = buildValueFromType(fieldInfo.fieldType, dict)
        }
    }
    return obj
}

export const buildJSON = (entities: Array<Entity>): string => {
    const handledEntities: Array<Entity> = new Array<Entity>()
    const newDict: Map<string, Entity> = new Map<string, Entity>()

    const entity = mergeEntity(entities, handledEntities)
    for (const item in handledEntities) {
        newDict[handledEntities[item].className] = handledEntities[item]
    }

    return JSON.stringify(buildObject(entity, newDict), null, 4)
}

const strArrayToString = (array: Array<string>): string => {
    let result = ''
    for (const i in array) {
        result += array[i] + '\n'
    }
    return result.trimEnd()
}

const identStr = (str: string, len: number): string => {
    const chars = '                                           '
    return chars.substring(0, len) + str
}

const isGenericType = (fieldType: string): boolean => {
    return fieldType.includes('<') && fieldType.includes('>')
}

const buildTsFields = (
    entity: Entity,
    level?: number
): Array<string> => {
    const result: Array<string> = new Array<string>()
    for (const i in entity.fieldList) {
        const field = entity.fieldList[i]
        const fieldType = field.fieldType
        result.push(
            identStr(field.fieldName, (level ? level : 0) * 2) +
                (field.require ? ':' : '?:') +
                javaTypeToTsType(fieldType)
        )
    }
    return result
}

export const generateClassName = (entity: Entity): string => {
    let className = FormatUtils.longTypeNameToShort(entity.className)
    if (javaToTsTypeMap.has(className)) {
        className = javaToTsTypeMap.get(className)
    }
    if (entity.generic && entity.generic.length > 0) {
        return className + '<' + entity.generic.join(',') + '>'
    }
    return className
}

export const buildTsEntitiesScript = (entities: Array<Entity>, ignores: Array<string>): string => {
    const result: Array<string> = new Array<string>()

    for (const i in entities) {
        if (!ignores || !ignores.includes(javaTypeToTsType(entities[i].className))) {
            result.push('export type ' + generateClassName(entities[i]) + ' = {')
            result.push(...buildTsFields(entities[i], 1))
            result.push('}\r\n')
        }
    }

    return strArrayToString(result)
}

export const buildTsMethod = (
    requestPath: string,
    method: ApiMethod,
    level?: number
): Array<string> => {
    log.info('tsMethod', method)
    const result = new Array<string>()
    if (method.requestMethod === 'POST' && method.postDataType === 'JSON') {
        const resultType = genericClassChainToStr(method.resultType, method.resultGenerics)
        if (method.description) {
            result.push(`/** \r\n *${method.description} \r\n*/`)
        }
        if (!method.parameters.type) {
            result.push(`export async${upperFirst(method.methodName)}=async ():${resultType}=>{`)
        } else {
            result.push(
                `export async${upperFirst(method.methodName)}=async (params:${javaTypeToTsType(method.parameters.type)}):${resultType}=>{`
            )
        }
        result.push(
            identStr(
                `return await http.asyncPost<${resultType}>('${PathUtils.formatPath(requestPath + '/' + method.path)}',params)`,
                2
            )
        )
        result.push('}')
    }

    //todo post form

    if (method.requestMethod == 'GET') {
        const resultType = genericClassChainToStr(method.resultType, method.resultGenerics)
        result.push(
            `export async${upperFirst(method.methodName)}=async (params:${javaTypeToTsType(method.parameters.type)}):${resultType}=>{`
        )
        result.push(
            identStr(
                `return await http.asyncGet<${resultType}>('${PathUtils.formatPath(requestPath + '/' + method.path)}',params)`,
                2
            )
        )
        result.push('}')
    }
    for (const i in result) {
        result[i] = identStr(result[i], level ? level : 0)
    }
    return result
}

export const buildTsDocument = (document: ApiDocument): Array<string> => {
    const result: Array<string> = new Array<string>()

    const reference: Array<string> = new Array<string>()

    document.methodList.forEach((value) => {
        try {
            const shortResultTypename = javaTypeToTsType(value.resultType)
            if (!reference.includes(shortResultTypename)) {
                reference.push(shortResultTypename)
            }

            if (value.resultGenerics) {
                value.resultGenerics.forEach((valueGeneric) => {
                    const shortTypename = javaTypeToTsType(valueGeneric)
                    if (!reference.includes(shortTypename)) {
                        reference.push(shortTypename)
                    }
                })
            }

            if (value.parameters) {
                if (Array.isArray(value.parameters)) {
                    value.parameters.forEach((valueParameter) => {
                        const shortTypename = javaTypeToTsType(valueParameter.type)
                        if (!reference.includes(shortTypename)) {
                            reference.push(shortTypename)
                        }
                    })
                } else {
                    const shortTypename = javaTypeToTsType(value.parameters.type)
                    if (!reference.includes(shortTypename)) {
                        reference.push(shortTypename)
                    }
                }
            }

            result.push(...buildTsMethod(document.path, value))
            result.push('\r\n')
        } catch (err) {
            log.error(err)
        }
    })

    if (reference.length > 0) {
        let referenceStr = 'import {\r\n'
        reference.forEach((value) => {
            referenceStr += identStr(value + ', \r\n', 2)
        })
        referenceStr = referenceStr.substring(0, referenceStr.length - 1) + "} from './entities.ts'"
        result.unshift(referenceStr)
    }
    return result
}

export const buildEntitiesFile = (path: string, project: ApiDocuments) => {
    try {
        const fileData = buildTsEntitiesScript(project.entities, ['array', 'Array'])
        writeTextFile({
            path: path,
            filename: 'entities.ts',
            textData: fileData
        })
    } catch (error) {
        log.error(error)
    }
}

export const buildTsProject = (path: string, project: ApiDocuments) => {
    buildEntitiesFile(path, project)
    project.documents.forEach((value) => {
        const data = buildTsDocument(value)
        writeTextFile({
            path,
            filename: `api${formatFileName(value.path)}.ts`,
            textData: strArrayToString(data)
        })
    })
}
